[原创]FRIDA 使用经验交流分享

您所在的位置:网站首页 frida插件 github [原创]FRIDA 使用经验交流分享

[原创]FRIDA 使用经验交流分享

2023-07-22 22:42| 来源: 网络整理| 查看: 265

Frida 是非常灵活的 Hook 框架,支持多平台。在这里就不过多介绍了,详细可以参看官网:https://frida.re/使用 Frida也挺长时间了,结合平时实战的经验,系统的梳理了一下开发环境、调试环境、整合的常用脚本和新特性分享给大家,在这里抛个砖。

TypeScript 开发环境

一开始使用 frida 时,直接写个 js 脚本然后注入就完事了,优点是快捷方便,缺点也很明显就是 hook 代码写多了都挤在一个 js 里冗长,事后阅读起来比较费事,也不利于模块化和代码复用。TypeScript + npm 的方式搭建 frida 开发环境,使得在用 IDE 编写 frida 时可以有代码补全提示同时 TypeScript 又能使开发更容易达到模块化,代码复用更方便。

优点 JavaScript的一个超集,扩展了JavaScript的语法。 加强代码可读性。明确参数类型,代码语义更清晰易懂。 更友好、更精准的代码补全提示。 更贴近面向对象编程的编写习惯,利于模块化和复用。 以前用 js 写的脚本也可以被 ts 直接引用,不会浪费。 环境搭建

Frida 使用 TypeScript, 环境的搭建很简单。

github 直接下载老奥的示例仓库: https://github.com/oleavr/frida-agent-example 按说明构建环境123$ git clone git://github.com/oleavr/frida-agent-example.git$ cd frida-agent-example/$ npm install实际上就是利用 frida-compile 编译脚本 frida-compile agent/index.ts -o _agent.js -c。

用 pycharm 打开构建好环境的工程,编写 frida ts 脚本已经可以看到代码补全提示了。

 

启用实时编译 123$ npm run watch# 或者$ frida-compile agent/index.ts -o _agent.js -w使用

将上面生成的目标文件 _agent.js 注入到目标进程即可。

1$ frida -U -f com.example.android --no-pause -l _agent.jsJS单步调试

能愉快的单步调试 frida 的 js 脚本,可以方便不少。

 

首先运行 frida 脚本

1frida -l --debug --runtime=v8

或者

123session = dev.attach(app.pid)script = session.create_script(jscode, runtime="v8")session.enable_debugger()

启动后会回显 Inspector 正在监听 9229 默认端口

1Chrome Inspector server listening on port 9229chrome

打开 chrome://inspect, 点击 Open dedicated DevTools for Node

 

 

此时 debug 已经连接,切换至 Sources,按 Command + P 加载要调试的脚本,即可下断调试了。

 

pycharm 首先安装 Node.js 插件,重启

添加调试器 Attaching to Node.js/Chrome,端口默认即可。Attach to 应选择 Node.js < 8 started with --debug, 下面的自动重连选项可选可不选。

触发断点需要在 debug 窗口切换到 script 选项卡,右键要调试的脚本,选择 Open Actual Source,在新打开的 Actual Source 窗口设置好断点后,需要再取消/启用一次所有断点作为激活,发现断点上打上对勾才真正可用了。

接下来就可以愉快的调试了

优缺点 用 Chrome 调试支持的更为顺滑,调试脚本自动重加载,断点也能正确响应。 用 PyCharm 调试断点有时需要手动激活有点麻烦,纯粹是个人偏爱PyCharm 的Debug 窗口和快捷键。 PyCharm 使用 ts 环境,调试时可以直接在 ts 文件上下断,也不需要手动激活断点,比较顺畅。 FridaContainer 脚本集分享

FridaContainer 整合了网上流行的和平时常用的 frida 脚本,为逆向工作提效之用。该仓库已经申请了开源,地址:https://github.com/deathmemory/FridaContainer

反反调试 anti-anti-debug

整理了一些普通反调试的绕过或定位的方法。因为很多反调试的手段是通过读取各个状态文件,查找特征字符串来判断是否被调试,而读取文件内容又通常都用到了 fgets 函数,如此我们就直接 hook 此函数加入过滤规则就能过滤掉许多反调试检测。

 

例如:/proc//status 检测 TracerPid: 0 、 State: S (sleeping) 、 SigBlk: 0000000000001204 ,/proc//stat 检测 t (tracing stop)/proc//wchan 检测 SyS_epoll_wait 等

 

Hook fgets 后,触发时首先获取一下 LR 寄存器的值,保存一下现场,之后返回信息时可以把 LR 带出来,方便定位。然后调用原函数,对赋值的 buffer 进行过滤就可以了。

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152static anti_fgets() {  const tag = 'anti_fgets';  const fgetsPtr = Module.findExportByName(null, 'fgets');  DMLog.i(Anti.tag, 'fgets addr: ' + fgetsPtr);  if (null == fgetsPtr) {    return;  }  var fgets = new NativeFunction(fgetsPtr, 'pointer', ['pointer', 'int', 'pointer']);  Interceptor.replace(fgetsPtr, new NativeCallback(function (buffer, size, fp) {    if (null == this) {      return 0;    }    var logTag = null;    // 进入时先记录现场    const lr = FCCommon.getLR(this.context);    // 读取原 buffer    var retval = fgets(buffer, size, fp);    var bufstr = (buffer as NativePointer).readCString();     if (null != bufstr) {      if (bufstr.indexOf("TracerPid:") > -1) {        buffer.writeUtf8String("TracerPid:\t0");        logTag = 'TracerPid';      }      //State:    S (sleeping)      else if (bufstr.indexOf("State:\tt (tracing stop)") > -1) {        buffer.writeUtf8String("State:\tS (sleeping)");        logTag = 'State';      }      // ptrace_stop      else if (bufstr.indexOf("ptrace_stop") > -1) {        buffer.writeUtf8String("sys_epoll_wait");        logTag = 'ptrace_stop';      }      else if (bufstr.indexOf(") t") > -1) {        buffer.writeUtf8String(bufstr.replace(") t", ") S"));        logTag = 'stat_t';      }       // SigBlk      else if (bufstr.indexOf('SigBlk:') > -1) {        buffer.writeUtf8String('SigBlk:\t0000000000001204');        logTag = 'SigBlk';      }      if (logTag) {        DMLog.i(tag + " " + logTag, bufstr + " -> " + buffer.readCString() + ' lr: ' + lr                + "(" + FCCommon.getModuleByAddr(lr) + ")");      }    }    return retval;  }, 'pointer', ['pointer', 'int', 'pointer']));}

FridaContainer 调用:

1FCAnd.anti.anti_debug();

小结:

 

当上面的方法无法绕过反调试时,可以再 Hook 一些常用的退出函数来定位反调点,比如 hook exit, kill ,再总结出一些其他过反调的方法,思路类似。

anti-ssl-pinning

我们也在 FridaContainer 里面集成了 Frida 版的 JustTrustMe 来过 SSL Pinning 检测。

 

此部分代码主要借鉴了:https://codeshare.frida.re/@akabe1/frida-multiple-unpinning/

 

支持 20 种类库的SSL 验证绕过:

TrustManager (Android < 7) TrustManagerImpl (Android > 7) OkHTTPv3 (quadruple bypass) Trustkit (triple bypass) Appcelerator Titanium OpenSSLSocketImpl Conscrypt OpenSSLEngineSocketImpl Conscrypt OpenSSLSocketImpl Apache Harmony PhoneGap sslCertificateChecker IBM MobileFirst pinTrustedCertificatePublicKey (double bypass) IBM WorkLight (ancestor of MobileFirst) HostNameVerifierWithCertificatePinning (quadruple bypass) Conscrypt CertPinManager CWAC-Netsecurity (unofficial back-port pinner for Android {    dest_cls.forEach((destCls) => {        // 按规则匹配是否需要 trace        if (match(destCls, curClsName)) {            // trace 核心方法            traceArtMethodsCore(curClsName);            return false; // end forEach        }    });});// Hook 核心逻辑function traceArtMethodsCore(clsname: string) {    let cls = Java.use(clsname);    // 枚举方法    let methods = cls.class.getDeclaredMethods();    methods.forEach(function (method: any) {        ...        // 枚举重载        let methodOverloads = cls[methodName].overloads;        methodOverloads.forEach(function (overload: any) {            ...            // Hook            overload.implementation = function () {                // ... send entry msg                // 利用 js 参数特性 arguments ,调用原函数以适配所有 Hook 方法的传参                const retval = this[methodName].apply(this, arguments);                // ... send exit msg                return retval;            }        }    }}

通过 python/android/traceLogCleaner.py 脚本收集 trace 日志,将回传的日志按线程、格式化输出日志,并且对字节数组,尝试进行 string 和 hex 转换以方便搜索。

 

格式化 trace 效果:

 

 

单条日志:

 

 

小结:

 

优点:用 Frida 做 java 方法级的 trace ,优点就是方便、灵活、轻量级。缺点:限于 Frida 框架,该方式效率较低,trace 方法过多容易崩溃,所以无法做全量 trace。推荐用于轻量级的 Java 方法 trace 可以有效的定位核心算法。

jni hook & trace

利用 Frida 的 Java.vm.getEnv() 获取 JNIEnv 指针,再根据 jni 结构体函数偏移,可以获取到各个 jni 函数地址,之后就可以根据需要进行 Hook 了。例如

可以将其封装成更便捷的获取各 jni 函数地址的功能,方便 Hook

核心逻辑:

123456789101112131415161718192021// 列出 JNI 函数数组const jni_struct_array = [    "reserved0",    "reserved1",    "reserved2",    "reserved3",    "GetVersion",    "DefineClass",    "FindClass",    "FromReflectedMethod",    ...];// 获取 JNIEnv 地址var env = Java.vm.getEnv();var env_ptr = env.handle.readPointer();// 根据函数名计算索引偏移var offset = jni_struct_array.indexOf(func_name) * Process.pointerSize;// 读取函数地址jnienv_addr.add(offset).readPointer();// HookInterceptor.attach(addr, callbacksOrProbe);

应用:

12345FCAnd.jni.hookJNI('NewStringUTF', {    onEnter: function (args) {      ...    }}); 可以 Hook RegistNatives 来获取动态注册的 jni 函数地址 12345678910111213141516171819202122232425262728293031323334353637383940414243export function hook_registNatives() {    const tag = 'fridaRegstNtv';    Jni.hookJNI("RegisterNatives", {        onEnter: function (args) {            var env = Java.vm.getEnv();            var p_size = Process.pointerSize;            var methods = args[2];            var methodcount = args[3].toInt32();            // 获取类名            var name = env.getClassName(args[1]);            DMLog.i(tag, "==== class: " + name + " ====");            DMLog.i(tag, "==== methods: " + methods + " nMethods: " + methodcount + " ====");            /** 根据函数结构原型遍历动态注册信息             typedef struct {                const char* name;                const char* signature;                void* fnPtr;             } JNINativeMethod;             jint RegisterNatives(JNIEnv* env, jclass clazz, const JNINativeMethod* methods, jint nMethods)             */            for (var i = 0; i < methodcount; i++) {                var idx = i * p_size * 3;                var fnPtr = methods.add(idx + p_size * 2).readPointer();                const module = Process.getModuleByAddress(fnPtr);                if (module) {                    const modulename = module.name;                    const modulebase = module.base;                    var logstr = "name: " + methods.add(idx).readPointer().readCString()                        + ", signature: " + methods.add(idx + p_size).readPointer().readCString()                        + ", fnPtr: " + fnPtr                        + ", modulename: " + modulename + " -> base: " + modulebase;                    if (null != modulebase) {                        logstr += ", offset: " + fnPtr.sub(modulebase);                    }                    DMLog.i(tag, logstr);                }                else {                    DMLog.e(tag, 'module is null');                }            }        }    });}

返回效果

12345678910111213==== class: com.xxxx.class.name ======== methods: 0xcd52d428 nMethods: 41 ====[INFO][fridaRegstNtv]: name: initialize, signature: ()V, fnPtr: 0xcd50b6bd, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x66bd[INFO][fridaRegstNtv]: name: onExit, signature: ()V, fnPtr: 0xcd50b6c7, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x66c7[INFO][fridaRegstNtv]: name: getMMKVWithID, signature: (Ljava/lang/String;ILjava/lang/String;)J, fnPtr: 0xcd50b6d1, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x66d1                  [INFO][fridaRegstNtv]: name: encodeBool, signature: (JLjava/lang/String;Z)Z, fnPtr: 0xcd50b76d, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x676d[INFO][fridaRegstNtv]: name: decodeBool, signature: (JLjava/lang/String;Z)Z, fnPtr: 0xcd50b7bf, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x67bf[INFO][fridaRegstNtv]: name: encodeInt, signature: (JLjava/lang/String;I)Z, fnPtr: 0xcd50b80f, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x680f[INFO][fridaRegstNtv]: name: decodeInt, signature: (JLjava/lang/String;I)I, fnPtr: 0xcd50b85b, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x685b[INFO][fridaRegstNtv]: name: encodeLong, signature: (JLjava/lang/String;J)Z, fnPtr: 0xcd50b8a5, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x68a5[INFO][fridaRegstNtv]: name: decodeLong, signature: (JLjava/lang/String;J)J, fnPtr: 0xcd50b8f7, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x68f7[INFO][fridaRegstNtv]: name: encodeFloat, signature: (JLjava/lang/String;F)Z, fnPtr: 0xcd50b953, modulename: libxxxx.so -> base: 0xcd505000, offset: 0x6953......

FridaContainer 调用:

1FCAnd.jni.hook_registNatives();

此功能因功能比较固定、使用频率高,也单独拆出了一个仓库:https://github.com/deathmemory/fridaRegstNtv

另外 jni 的部分,还可以做成 jni trace

这里参考了 jnitrace 做了简化和嵌入版。

 

具体实现:

 

因为有了上面的基础,使 Jni 的 trace 变得简洁了

1234567891011121314151617181920export function traceAllJNISimply() {    // 遍历 Hook Jni 函数    jni_struct_array.forEach(function (func_name, idx) {        if (!func_name.includes("reserved")) {            Jni.hookJNI(func_name, {                onEnter(args) {                    // 触发时将信息保存到对象中                    let md = new MethodData(this.context, func_name, JNI_ENV_METHODS[idx], args);                    this.md = md;                },                onLeave(retval) {                    // 退出时将返回值追加到对象中                    this.md.setRetval(retval);                    // 发送日志                    send(JSON.stringify({tid: this.threadId, status: "jnitrace", data: this.md}));                }            });        }    })}

因为性能问题,在信息收集时去除了 jnitrace 中的 Thread.backtrace 和 DebugSymbol.fromAddress 可能会造成线程阻塞的因素,对 trace 效果没有太多影响。

 

格式化日志输出效果:

 

 

FridaContainer 调用:

1FCAnd.jni.traceAllJNISimply();

参考:jnitrace

 

小结:jni 的 trace 做了速度上的优化,去除了一些线程阻塞的因素,使得在 jni 全量 trace 下也可以很好的运行。结合上面的 Java trace ,寻常难度的算法定位效率可以有一个很好的提高。

Stalker 的应用

Stalker 部分的内容,bmax 大佬已经发了一篇贴子,大家可以跳转看一下。地址:https://bbs.pediy.com/thread-264680.htm

Frida 的检测方式 文件名 frida-agent** 默认端口 27042 特征字符 frida:rpc、LIBFRIDA 端口应答特征 1234567891011121314if (connect(sock , (struct sockaddr*)&sa , sizeof sa) != -1) {  memset(res, 0 , 7);   send(sock, "\x00", 1, NULL);  send(sock, "AUTH\r\n", 6, NULL);   usleep(100); // Give it some time to answer   if ((ret = recv(sock, res, 6, MSG_DONTWAIT)) != -1) {    if (strcmp(res, "REJECT") == 0) {      __android_log_print(ANDROID_LOG_VERBOSE, APPNAME,  "FRIDA DETECTED [1] - frida server running on port %d!", i);    }  }}

参考:AntiFrida

总结

以上是我们对使用 Frida 经验的一些总结。在逆向工程里,个人认为有两个方面对逆向的提效是帮助非常大的,一个是 trace 一个是算法识别。而 Frida 在有了 Stalker 的加持下,这两个方面都可以实现。在轻量级和便捷性上,首选推荐。

 

FridaContainer:https://github.com/deathmemory/FridaContainerfridaRegstNtv:https://github.com/deathmemory/fridaRegstNtv以上两个仓库还请大佬们多多拍砖,提建议。抛砖引玉,希望能和业界大佬多多交流。感谢。

看雪·2023 KCTF年度赛即将来袭! [防守方]规则发布,征题截止08月25日



【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3